// ========================================== // liquid-glass.js // Host this on your main server/CDN // ========================================== class LiquidGlassEngine { static init(config = {}) { this.bgUrl = config.backgroundUrl || ''; this.zIndex = config.zIndex || -1; this.injectCSS(); this.setupDOM(); this.loadBackground(); this.initWebGL(); } static injectCSS() { const style = document.createElement('style'); style.innerHTML = ` :root { --lg-c-light: #fff; --lg-c-dark: #000; --lg-reflex-dark: 2; --lg-reflex-light: 0.35; --lg-shadow: inset 0 0 0 1px color-mix(in srgb, var(--lg-c-light) calc(var(--lg-reflex-light) * 10%), transparent), inset 1.8px 3px 0px -2px color-mix(in srgb, var(--lg-c-light) calc(var(--lg-reflex-light) * 90%), transparent), inset -2px -2px 0px -2px color-mix(in srgb, var(--lg-c-light) calc(var(--lg-reflex-light) * 80%), transparent), inset -3px -8px 1px -6px color-mix(in srgb, var(--lg-c-light) calc(var(--lg-reflex-light) * 60%), transparent), inset -0.3px -1px 4px 0px color-mix(in srgb, var(--lg-c-dark) calc(var(--lg-reflex-dark) * 12%), transparent), inset -1.5px 2.5px 0px -2px color-mix(in srgb, var(--lg-c-dark) calc(var(--lg-reflex-dark) * 20%), transparent), inset 0px 3px 4px -2px color-mix(in srgb, var(--lg-c-dark) calc(var(--lg-reflex-dark) * 20%), transparent), inset 2px -6.5px 1px -4px color-mix(in srgb, var(--lg-c-dark) calc(var(--lg-reflex-dark) * 10%), transparent), 0px 1px 5px 0px color-mix(in srgb, var(--lg-c-dark) calc(var(--lg-reflex-dark) * 10%), transparent), 0px 6px 16px 0px color-mix(in srgb, var(--lg-c-dark) calc(var(--lg-reflex-dark) * 8%), transparent); --lg-dark-shadow: inset 0 0 0 1px rgba(255,255,255,0.05), inset 1.5px 2px 0px -1px rgba(255,255,255,0.15), inset -1px -1px 0px -1px rgba(255,255,255,0.1), inset -0.3px -1px 4px 0px rgba(0,0,0,0.8), inset 0px 3px 4px -2px rgba(0,0,0,0.6), 0px 4px 12px 0px rgba(0,0,0,0.5), 0px 12px 24px 0px rgba(0,0,0,0.7); } /* The Base Glass Class */ .liquid-glass { box-shadow: var(--lg-shadow), 0 20px 40px rgba(0,0,0,0.3); position: relative; /* ensure it sits above the canvas */ } /* The 6 Variants you requested */ .lg-clear { background: transparent; backdrop-filter: none; } .lg-light-clear { background: rgba(255, 255, 255, 0.1); backdrop-filter: none; } .lg-dark-clear { background: rgba(0,0,0,0.5); box-shadow: var(--lg-dark-shadow); backdrop-filter: none; color: rgba(255,255,255,0.8); } .lg-low-blur { background: rgba(255,255,255,0.02); backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px); } .lg-light-low-blur { background: rgba(255, 255, 255, 0.1); backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px); } .lg-dark-low-blur { background: rgba(0,0,0,0.4); box-shadow: var(--lg-dark-shadow); backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px); color: rgba(255,255,255,0.8); } `; document.head.appendChild(style); } static setupDOM() { this.bgCanvas = document.createElement('canvas'); this.bgCanvas.style.display = 'none'; document.body.appendChild(this.bgCanvas); this.glCanvas = document.createElement('canvas'); this.glCanvas.style.cssText = `position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; z-index: ${this.zIndex}; pointer-events: none;`; document.body.appendChild(this.glCanvas); this.bgCtx = this.bgCanvas.getContext("2d"); this.gl = this.glCanvas.getContext("webgl", { antialias: true }); } static loadBackground() { this.img = new Image(); this.img.crossOrigin = "anonymous"; this.img.src = this.bgUrl; } static initWebGL() { const gl = this.gl; const vsSource = `attribute vec2 position; void main() { gl_Position = vec4(position, 0.0, 1.0); }`; const fsSource = ` precision mediump float; uniform vec3 iResolution; uniform sampler2D iChannel0; uniform int u_elemCount; uniform vec4 u_elems[30]; uniform float u_radii[30]; void mainImage(out vec4 fragColor, in vec2 fragCoord) { vec2 p = fragCoord; vec4 accumulatedLighting = vec4(0.0); float accumulatedMask = 0.0; for (int i = 0; i < 30; i++) { if (i >= u_elemCount) break; vec4 rect = u_elems[i]; float radius = u_radii[i]; vec2 center_px = vec2(rect.x + rect.z * 0.5, iResolution.y - (rect.y + rect.w * 0.5)); vec2 local_p = p - center_px; vec2 b = vec2(rect.z * 0.5, rect.w * 0.5); float r = min(radius, min(b.x, b.y)); vec2 d = abs(local_p) - b + vec2(r); float dist_px = min(max(d.x, d.y), 0.0) + length(max(d, 0.0)) - r; float size_px = max(min(b.x, b.y), 1.0); float t = max((dist_px / size_px) * 0.215443 + 0.215443, 0.0); float rb = pow(abs(t), 6.0); float rb1 = clamp((1.0 - rb * 10000.0) * 8.0, 0.0, 1.0); float rb2 = clamp((0.95 - rb * 9500.0) * 16.0, 0.0, 1.0) - clamp(pow(0.9 - rb * 9500.0, 1.0) * 16.0, 0.0, 1.0); float rb3 = clamp((1.5 - rb * 11000.0) * 2.0, 0.0, 1.0) - clamp(pow(1.0 - rb * 11000.0, 1.0) * 2.0, 0.0, 1.0); float transition = smoothstep(0.0, 1.0, rb1 + rb2); if (transition > 0.0) { p = center_px + (local_p * (1.0 - rb * 5000.0)); // Refract float best_m2_y = (local_p.y / max(b.y, 1.0)) * 0.215443; float gradient = clamp((clamp(best_m2_y, 0.0, 0.2) + 0.1) / 2.0, 0.0, 1.0) + clamp((clamp(-best_m2_y, -1000.0, 0.2) * rb3 + 0.1) / 2.0, 0.0, 1.0); vec4 lighting = vec4(rb1) * gradient + vec4(rb2) * 0.3; accumulatedLighting += lighting * transition * (1.0 - accumulatedMask); accumulatedMask += transition * (1.0 - accumulatedMask); } } vec4 baseColor = texture2D(iChannel0, p / iResolution.xy); vec4 originalColor = texture2D(iChannel0, fragCoord / iResolution.xy); fragColor = mix(originalColor, clamp(baseColor + accumulatedLighting, 0.0, 1.0), accumulatedMask); } void main() { mainImage(gl_FragColor, gl_FragCoord.xy); } `; const createShader = (type, source) => { const shader = gl.createShader(type); gl.shaderSource(shader, source); gl.compileShader(shader); return shader; }; const program = gl.createProgram(); gl.attachShader(program, createShader(gl.VERTEX_SHADER, vsSource)); gl.attachShader(program, createShader(gl.FRAGMENT_SHADER, fsSource)); gl.linkProgram(program); gl.useProgram(program); const buffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, buffer); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]), gl.STATIC_DRAW); const position = gl.getAttribLocation(program, "position"); gl.enableVertexAttribArray(position); gl.vertexAttribPointer(position, 2, gl.FLOAT, false, 0, 0); this.uniforms = { resolution: gl.getUniformLocation(program, "iResolution"), texture: gl.getUniformLocation(program, "iChannel0"), elemCount: gl.getUniformLocation(program, "u_elemCount"), elems: gl.getUniformLocation(program, "u_elems"), radii: gl.getUniformLocation(program, "u_radii") }; this.texture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, this.texture); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true); this.render(); } static updateBackgroundTexture() { const w = this.bgCanvas.width, h = this.bgCanvas.height; if (this.img.complete && this.img.naturalWidth > 0) { const cA = w / h, iA = this.img.naturalWidth / this.img.naturalHeight; let dW, dH, dX, dY; if (cA > iA) { dW = w; dH = w / iA; dX = 0; dY = (h - dH) / 2; } else { dH = h; dW = h * iA; dX = (w - dW) / 2; dY = 0; } this.bgCtx.drawImage(this.img, dX, dY, dW, dH); } else { this.bgCtx.fillStyle = '#111'; this.bgCtx.fillRect(0, 0, w, h); } } static render = () => { const dpr = window.devicePixelRatio || 1; const cw = Math.round(this.glCanvas.clientWidth * dpr); const ch = Math.round(this.glCanvas.clientHeight * dpr); if (cw > 0 && ch > 0) { if (this.glCanvas.width !== cw || this.glCanvas.height !== ch) { this.glCanvas.width = cw; this.glCanvas.height = ch; this.bgCanvas.width = cw; this.bgCanvas.height = ch; } this.updateBackgroundTexture(); this.gl.bindTexture(this.gl.TEXTURE_2D, this.texture); this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.gl.RGBA, this.gl.RGBA, this.gl.UNSIGNED_BYTE, this.bgCanvas); this.gl.viewport(0, 0, this.glCanvas.width, this.glCanvas.height); this.gl.uniform3f(this.uniforms.resolution, this.glCanvas.width, this.glCanvas.height, 1.0); // This line automatically targets ANY element on the host website with the class 'liquid-glass' const elements = Array.from(document.querySelectorAll('.liquid-glass')); // Sort by z-index so glass on top refracts glass below it properly elements.sort((a, b) => (window.getComputedStyle(a).zIndex || 0) - (window.getComputedStyle(b).zIndex || 0)); const sortedForShader = elements.reverse(); const uElemsData = new Float32Array(30 * 4), uRadiiData = new Float32Array(30); let elemCount = 0; sortedForShader.forEach((el, i) => { if (i >= 30) return; const rect = el.getBoundingClientRect(); uElemsData[i * 4 + 0] = rect.left * dpr; uElemsData[i * 4 + 1] = rect.top * dpr; uElemsData[i * 4 + 2] = rect.width * dpr; uElemsData[i * 4 + 3] = rect.height * dpr; let radiusStr = window.getComputedStyle(el).borderRadius; let radius = parseFloat(radiusStr); if (radiusStr.includes('%')) radius = (Math.min(rect.width, rect.height) / 2); uRadiiData[i] = radius * dpr; elemCount++; }); this.gl.uniform1i(this.uniforms.elemCount, elemCount); this.gl.uniform4fv(this.uniforms.elems, uElemsData); this.gl.uniform1fv(this.uniforms.radii, uRadiiData); this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4); } requestAnimationFrame(this.render); } }